【翻译】SpringBoot 虚拟线程 VS WebFlux :JWT 验证和 MySQL 查询的性能比较
摘要
在基准测试中,Spring WebFlux 在验证 MYSQL 数据库中的 JWT 令牌时,比使用虚拟线程的 Spring Boot 快 57%。看起来我们没有从使用虚拟线程中获得任何好处……
介绍
在对一系列技术(包括 Node.js、Deno、Bun、Rust、Go、Spring、Python 等)在简单的“hello world”场景中的性能进行广泛评估后,我逐渐收到了很多的反馈。尽管这些文章获得了不少人的赞同,但常有读者反映,它们都没有直接涉及实际业务的用例。读者们敦促我将分析扩展到更实际的场景。令人惊讶的是,这些文章依然吸引了大量的阅读量。尽管如此,这一点意见确实有其合理性。作为起点, “hello world” 是理想的,但它远不能代表现实世界的复杂性。
现实实际用例
本文是我们持续系列的一部分,旨在通过现实世界的场景来剖析各种技术。在这个具体案例中,我们将深入探讨以下常见的用例:
- 从 authorization header提取 JWT。
- 验证 JWT 并从其claims中提取用户的电子邮件。
- 使用提取的电子邮件执行 MySQL 查询。
- 最后返回用户的记录。
尽管这个场景看起来简单,但它代表了在 web 开发领域中经常遇到的一个实际挑战。
技术部分
在本文中,我们将深入比较SpringBoot(带虚拟线程)和WebFlux这对同胞兄弟,重点关注它们在特定用例场景中的性能。我们已经探讨了标准 SpringBoot 应用程序与 Webflux 的性能对比,现在我们将介绍一个关键的区别因素:
SpringBoot(带虚拟线程)
我们有 SpringBoot,但有一点不同——它在虚拟线程而不是传统的物理线程上运行。虚拟线程是并发领域的游戏规则改变者。这些轻量级线程简化了开发、维护和调试高吞吐量并发应用程序的复杂任务。虽然虚拟线程仍然在底层操作系统线程上运行,但它们带来了显着的效率改进。当虚拟线程遇到阻塞 I/O 操作时,Java 运行时会暂时挂起它,从而释放关联的操作系统线程来为其他虚拟线程提供服务。这个优雅的解决方案优化了资源分配并增强了整体应用程序响应能力。
SpringBoot Webflux
另一边是 Spring Boot Webflux。Spring Boot WebFlux 是 Spring 生态系统中的一个反应式编程框架,旨在构建高度可扩展的异步网络应用程序。它利用 Project Reactor 库实现非阻塞、事件驱动编程。Spring Boot WebFlux 尤其适合需要高并发性和低延迟的应用,是构建反应式微服务和实时数据密集型应用的绝佳选择。基于其响应式,开发人员可以高效地处理大量并发请求,同时还能灵活地集成各种数据源和通信协议。
考虑到这些有趣的特性,让我们更深入地研究我们的性能比较。
测试环境和软件版本
我们的性能测试是在配备 16GB 内存的 MacBook Pro M1 上进行的,以确保测试 平台的可靠性。测试中使用的软件包括:
- SpringBoot 3.1.3 (Running on Java 20)
- 预览模式下使用虚拟线程
SpringBoot 和使用的库
我们的设置包括使用以下关键组件:
- SpringBoot 3.1.3 + Java 20 (预览模式下使用虚拟线程)
- jjwt for JWT 验证和解码, 对我们的应用增强安全性校验.
- mysql-connector-java 执行 MySQL 查询, 维护数据完整性和一致性.
负载测试和 JWT
为了评估我们的应用在不同负载下的性能,我们使用了开源负载测试工具 Bombardier。我们的测试场景包括一个预先创建的 100,000 个 JWT 的列表。在测试过程中,Bombardier 从这个池中随机选择 JWT,并将其包含在 HTTP 请求的 authorization header
中。
MySQL 数据库结构
用于这些性能测试的 MySQL 数据库包含一个名为 users
的表。该表设计有 6 列,足 以模拟我们应用中的现实世界数据交互,使我们能够评估它们的响应性和可扩展性。
mysql> desc users;
+--------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| email | varchar(255) | NO | PRI | NULL | |
| first | varchar(255) | YES | | NULL | |
| last | varchar(255) | YES | | NULL | |
| city | varchar(255) | YES | | NULL | |
| county | varchar(255) | YES | | NULL | |
| age | int | YES | | NULL | |
+--------+--------------+------+-----+---------+-------+
6 rows in set (0.00 sec)
用户数据库的初始数据集包含大量 100,000 条用户记录
mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
| 99999 |
+----------+
1 row in set (0.01 sec)
在我们对 SpringBoot 虚拟线程和 Webflux 进行性能评估的过程中,有必要了解一个重要的数据关系。具体来说,在 JSON Web 令牌(JWT)payload中,每个电子邮件记录都与 MySQL 数据库中存储的用户记录直接对应。
代码部分
SpringBoot (virtual threads)
配置
server.port=3000
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username=testuser
spring.datasource.password=testpwd
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
业务代码
package com.example.demo;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
@Table(name = "users")
public class User {
@Id
private String email;
private String first;
private String last;
private String city;
private String county;
private int age;
public String getId() {
return email;
}
public void setId(String email) {
this.email = email;
}
public String getFirst() {
return first;
}
public void setFirst(String name) {
this.first = name;
}
public String getLast() {
return last;
}
public void setLast(String name) {
this.last = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCounty() {
return county;
}
public void setCounty(String county) {
this.county = county;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import com.example.demo.UserRepository;
import com.example.demo.User;
@RestController
public class UserController {
@Autowired
UserRepository userRepository;
private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
private String jwtSecret = System.getenv("JWT_SECRET");
@GetMapping("/")
public User handleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
String jwtString = authHdr.replace("Bearer","");
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret.getBytes())
.parseClaimsJws(jwtString).getBody();
Optional<User> user = userRepository.findById((String)claims.get("email"));
return user.get();
}
}
SpringBoot Webflux
配置
server.port=3000
spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb
spring.r2dbc.username=dbser
spring.r2dbc.password=dbpwd
业务代码
package webfluxdemo;
import org.springframework.data.annotation.Id;
public class User {
@Id
private String email;
private String first;
private String last;
private String city;
private String county;
private int age;
public User() {
}
public User(String email, String first, String last, String city, String county, int age) {
this.email = email;
this.first = first;
this.last = last;
this.city = city;
this.county = county;
this.age = age;
}
public String getId() {
return email;
}
public void setId(String email) {
this.email = email;
}
public String getFirst() {
return first;
}
public void setFirst(String name) {
this.first = name;
}
public String getLast() {
return last;
}
public void setLast(String name) {
this.last = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCounty() {
return county;
}
public void setCounty(String county) {
this.county = county;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package webfluxdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.HttpHeaders;
import webfluxdemo.User;
import webfluxdemo.UserService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/")
public class UserController {
@Autowired
UserService userService;
private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
private String jwtSecret = System.getenv("JWT_SECRET");
@GetMapping("/")
@ResponseStatus(HttpStatus.OK)
public Mono<User> getUserById(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
String jwtString = authHdr.replace("Bearer","");
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret.getBytes())
.parseClaimsJws(jwtString).getBody();
return userService.findById((String)claims.get("email"));
}
}
package webfluxdemo;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import webfluxdemo.User;
public interface UserRepository extends R2dbcRepository<User, String> {
}
package webfluxdemo;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import webfluxdemo.User;
import webfluxdemo.UserRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public Mono<User> findById(String id) {
return userRepository.findById(id);
}
}
结果
为了评估性能,我们进行了一系列严格的测试。每次测试都包含 500 万个请求,我们评估了这些请求在不同并发连接水平下的性能:50、100 和 300。
现在,让我们以简洁的表格形式来了解一下结果:
总结
计分卡是根据以下公式从结果中生成的。对于每个测量,计算获胜差距。如果获胜差距是:
- 小于5%,不计分
- 在5%到20%之间,胜方得1分
- 在20%到50%之间,胜方得2分
- 达到或超过50%,胜方得3分
即使是在像这样的实际案例中,使用虚拟线程似乎也不会比 webflux 更有优势。如果你愿意分享,请在评论中加入你对虚拟线程的专业意见。